﻿// Autore: Alessio Parma

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;

namespace RecipesManager.Data.Operations
{
    public static class Operations
    {
        public const string Grams = "Grammi";
        public const string Liters = "Litri";
        public const string Units = "Unità";

        #region Add/Remove Ingredients To/From List Methods

        // Situazioni in cui si ritorna un messaggio:
        // * Un ingrediente di una ricetta ha "quanto basta" come unità di misura
        public static IEnumerable<string> AddMenuToShoppingList(IEnumerable<string> recipeNames,
                                                                IEnumerable<int?> currServings)
        {
            Contract.Requires(recipeNames != null && currServings != null);
            Contract.Requires(recipeNames.Count() == currServings.Count());
            Contract.Requires(Contract.ForAll(recipeNames, n => Manager.Recipes.Contains(n)));
            Contract.Requires(Contract.ForAll(currServings, s => s != null && s > 0));
            Contract.Ensures(Contract.Result<IEnumerable<string>>() != null);

            return recipeNames.Zip(currServings, AddRecipeToShoppingList).SelectMany(l => l).ToList();
        }

        // Situazioni in cui si ritorna un messaggio:
        // * Un ingrediente della ricetta ha "quanto basta" come unità di misura
        public static IEnumerable<string> AddRecipeToShoppingList(string recipeName, int? currServings)
        {
            Contract.Requires(currServings != null);
            Contract.Requires(Manager.Recipes.Contains(recipeName));
            Contract.Ensures(Contract.Result<IEnumerable<string>>() != null);

            if (currServings == 0)
                return new[] {"La ricetta " + recipeName + " nell'evento corrente ha zero porzioni associate."};

            var recipe = Manager.Recipes[recipeName];
            // ReSharper madness...
            if (recipe.Servings == null)
                return new[] {"La ricetta " + recipeName + " non ha un numero di porzioni associato."};
            var ratio = currServings/(double) recipe.Servings;
            Debug.Assert(ratio != null);

            var ingredients = recipe.Ingredients;
            var messages = ingredients.Select(i => AddToIngredientListQuantity(i, ratio.Value));

            foreach (var s in recipe.Steps.Where(s => s.Type == StepType.SubRecipe))
            {
                var srServings = (int) Math.Ceiling((double) (ratio*s.Servings));
                messages = messages.Union(AddRecipeToShoppingList(s.SubRecipe.Name, srServings));
            }

            return messages.Where(m => m != null).ToList();
        }

        // Situazioni in cui si ritorna un messaggio:
        // * Un ingrediente della ricetta ha "quanto basta" come unità di misura
        // * Non vi è una quantità sufficiente di un ingrediente nella lista della spesa
        public static IEnumerable<string> RemoveRecipeFromShoppingList(string recipeName, int? currServings)
        {
            Contract.Requires(currServings != null && currServings > 0);
            Contract.Requires(Manager.Recipes.Contains(recipeName));
            Contract.Ensures(Contract.Result<IEnumerable<string>>() != null);

            var recipe = Manager.Recipes[recipeName];
            // ReSharper madness...
            Debug.Assert(recipe.Servings != null);
            var ratio = currServings/(double) recipe.Servings;
            Debug.Assert(ratio != null);

            var ingredients = recipe.Ingredients;
            var messages = ingredients.Select(i => RemoveFromIngredientListQuantity(i, ratio.Value));

            foreach (var s in recipe.Steps.Where(s => s.Type == StepType.SubRecipe))
            {
                var srServings = (int) Math.Ceiling((double) (ratio*s.Servings));
                messages = messages.Union(RemoveRecipeFromShoppingList(s.SubRecipe.Name, srServings));
            }

            return messages.Where(m => m != null).ToList();
        }

        #endregion

        #region CanRemoveEntity Methods

        // Elementi vincolanti per categoria:
        // * Le intolleranze degli ospiti
        // Elementi NON vincolanti per categoria:
        // * Le categorie padre e figlie
        // * Le preferenze degli ospiti
        // * Gli ingredienti che le appartengono
        [Pure]
        public static bool CanRemoveCategory(string categoryName, out IEnumerable<string> messages)
        {
            Contract.Requires(Manager.Categories.Contains(categoryName));
            Contract.Ensures(Contract.ValueAtReturn(out messages) != null);

            var c = Manager.Categories[categoryName];
            var s = new SortedSet<string>(c.GuestIntolerances.Select(g => g.Name));
            messages = s;
            if (s.Count > 0) return false;

            foreach (var cc in c.Parents.Unordered) s.Add(cc.Name);
            foreach (var cc in c.Children.Unordered) s.Add(cc.Name);
            foreach (var g in c.GuestRatings) s.Add(g.Name);
            foreach (var i in c.Ingredients.Unordered) s.Add(i.Name);
            return true;
        }

        // Elementi vincolanti per evento:
        // * NESSUNO
        // Elementi NON vincolanti per evento:
        // * NESSUNO
        [Pure]
        public static bool CanRemoveEvent(int eventId, out IEnumerable<string> messages)
        {
            Contract.Requires(Manager.Events.Contains(eventId));
            Contract.Ensures(Contract.ValueAtReturn(out messages) != null);

            messages = new SortedSet<string>();
            return true;
        }

        // Elementi vincolanti per ospite:
        // * Gli ospiti con cui ha una relazione
        // Elementi NON vincolanti per ospite:
        // * Le preferenze per altri ospiti
        // * Gli eventi a cui partecipa
        [Pure]
        public static bool CanRemoveGuest(string guestName, out IEnumerable<string> messages)
        {
            Contract.Requires(Manager.Guests.Contains(guestName));
            Contract.Ensures(Contract.ValueAtReturn(out messages) != null);

            var g = Manager.Guests[guestName];
            var s = new SortedSet<string>(g.GuestRelationships.Select(og => og.Name));
            messages = s;
            if (s.Count > 0) return false;

            foreach (var og in g.OtherGuestRatings) s.Add(og.Name);
            foreach (var e in g.EventsParticipations) s.Add(e.Name);
            return true;
        }

        // Elementi vincolanti per ingrediente:
        // * Le intolleranze degli ospiti
        // * Le ricette in cui figura come ingrediente
        // * Le ricette in cui figura come ingrediente originale
        // Elementi NON vincolanti per ingrediente:
        // * Le categorie a cui appartiene
        // * Le preferenze degli ospiti
        // * Le disponibilità nei negozi
        [Pure]
        public static bool CanRemoveIngredient(string ingredientName, out IEnumerable<string> messages)
        {
            Contract.Requires(Manager.Ingredients.Contains(ingredientName));
            Contract.Ensures(Contract.ValueAtReturn(out messages) != null);

            var i = Manager.Ingredients[ingredientName];
            var s = new SortedSet<string>(i.GuestIntolerances.Select(g => g.Name));
            messages = s;
            foreach (var r in i.Recipes) s.Add(r.Name);
            foreach (var r in i.OriginalRecipes) s.Add(r.Name);
            if (s.Count > 0) return false;

            foreach (var c in i.Categories) s.Add(c.Name);
            foreach (var g in i.GuestRatings) s.Add(g.Name);
            foreach (var sh in i.ShopRatings) s.Add(sh.Name);
            return true;
        }

        // Elementi vincolanti per ingrediente nella lista:
        // * NESSUNO
        // Elementi NON vincolanti per ingrediente nella lista:
        // * NESSUNO
        [Pure]
        public static bool CanRemoveIngredientFromList(string ingredientName, string measureName,
                                                       out IEnumerable<string> messages)
        {
            Contract.Requires(Manager.Ingredients.Contains(ingredientName));
            Contract.Requires(Manager.Measures.Contains(measureName));
            Contract.Ensures(Contract.ValueAtReturn(out messages) != null);

            messages = new SortedSet<string>();
            return true;
        }

        // Elementi vincolanti per ricetta:
        // * Le ricette in cui figura come sotto ricetta
        // * Gli eventi per cui è prevista
        // Elementi NON vincolanti per ricetta:
        // * Le preferenze degli ospiti
        [Pure]
        public static bool CanRemoveRecipe(string recipeName, out IEnumerable<string> messages)
        {
            Contract.Requires(Manager.Recipes.Contains(recipeName));
            Contract.Ensures(Contract.ValueAtReturn(out messages) != null);

            var r = Manager.Recipes[recipeName];
            var s = new SortedSet<string>(r.SubRecipeOf.Select(sr => sr.Name));
            messages = s;
            if (s.Count > 0) return false;

            foreach (var g in r.GuestRatings) s.Add(g.Name);
            return true;
        }

        // Elementi vincolanti per negozio:
        // * NESSUNO
        // Elementi NON vincolanti per negozio:
        // * Gli ingredienti disponibili
        [Pure]
        public static bool CanRemoveShop(string shopName, out IEnumerable<string> messages)
        {
            Contract.Requires(Manager.Shops.Contains(shopName));
            Contract.Ensures(Contract.ValueAtReturn(out messages) != null);

            var s = Manager.Shops[shopName];
            messages = new SortedSet<string>(s.IngredientRatings.Select(i => i.Name));
            return true;
        }

        #endregion

        #region GetAddableEntities Methods

        [Pure]
        public static IEnumerable<ICategory> GetAddableChildren(string categoryName, IEnumerable<string> currChildren,
                                                                IEnumerable<string> currParents)
        {
            Contract.Requires(currChildren != null && currParents != null);
            Contract.Requires(categoryName == "" || Manager.Categories.Contains(categoryName));
            Contract.Ensures(Contract.Result<IEnumerable<ICategory>>() != null);

            var unaddableChildren = new HashSet<string>(currChildren);
            if (categoryName != "") unaddableChildren.Add(categoryName);
            var stack = new Stack<ICategory>(currParents.Select(p => Manager.Categories[p]));
            while (stack.Count > 0)
            {
                var ct = stack.Pop();
                unaddableChildren.Add(ct.Name);
                foreach (var p in ct.Parents)
                    stack.Push(p);
            }
            return AddableEntities(Manager.Categories, unaddableChildren);
        }

        [Pure]
        public static IEnumerable<ICategory> GetAddableParents(string categoryName, IEnumerable<string> currChildren,
                                                               IEnumerable<string> currParents)
        {
            Contract.Requires(currChildren != null && currParents != null);
            Contract.Requires(categoryName == "" || Manager.Categories.Contains(categoryName));
            Contract.Ensures(Contract.Result<IEnumerable<ICategory>>() != null);

            var unaddableParents = new HashSet<string>(currParents) {categoryName};
            if (categoryName != "") unaddableParents.Add(categoryName);
            var stack = new Stack<ICategory>(currChildren.Select(p => Manager.Categories[p]));
            while (stack.Count > 0)
            {
                var ct = stack.Pop();
                unaddableParents.Add(ct.Name);
                foreach (var p in ct.Children)
                    stack.Push(p);
            }
            return AddableEntities(Manager.Categories, unaddableParents);
        }

// ReSharper disable PossibleMultipleEnumeration

        [Pure]
        public static IEnumerable<ICategory> GetAddableCategoriesForIngredient(string ingredientName,
                                                                               IEnumerable<string> currCategories)
        {
            AddableEntitiesContract<IIngredient, ICategory>(Manager.Ingredients, ingredientName, currCategories);
            return AddableEntities(Manager.Categories, new HashSet<string>(currCategories));
        }

        [Pure]
        public static IEnumerable<ICategory> AddableCategoriesForGuest(string guestName,
                                                                       IEnumerable<string> currCategories)
        {
            AddableEntitiesContract<IGuest, ICategory>(Manager.Guests, guestName, currCategories);
            return AddableEntities(Manager.Categories, new HashSet<string>(currCategories));
        }

        [Pure]
        public static IEnumerable<IIngredient> AddableIngredientsForGuest(string guestName,
                                                                          IEnumerable<string> currIngredients)
        {
            AddableEntitiesContract<IGuest, IIngredient>(Manager.Guests, guestName, currIngredients);
            return AddableEntities(Manager.Ingredients, new HashSet<string>(currIngredients));
        }

        [Pure]
        public static IEnumerable<IGuest> AddableGuestsForGuest(string guestName, IEnumerable<string> currGuests)
        {
            AddableEntitiesContract<IGuest, IGuest>(Manager.Guests, guestName, currGuests);
            var addedGuests = new HashSet<string>(currGuests) {guestName};
            return AddableEntities(Manager.Guests, addedGuests);
        }

        [Pure]
        public static IEnumerable<IRecipe> AddableRecipesForGuest(string guestName, IEnumerable<string> currRecipes)
        {
            AddableEntitiesContract<IGuest, IRecipe>(Manager.Guests, guestName, currRecipes);
            return AddableEntities(Manager.Recipes, new HashSet<string>(currRecipes));
        }

        [Pure]
        public static IEnumerable<IIngredient> GetAddableIngredientsForRecipe(string recipeName,
                                                                              IEnumerable<string> currIngredients)
        {
            AddableEntitiesContract<IRecipe, IIngredient>(Manager.Recipes, recipeName, currIngredients);
            return AddableEntities(Manager.Ingredients, new HashSet<string>(currIngredients));
        }

        [Pure]
        public static IEnumerable<IIngredient> GetAddableIngredientsForShop(string shopName,
                                                                            IEnumerable<string> currIngredients)
        {
            AddableEntitiesContract<IShop, IIngredient>(Manager.Shops, shopName, currIngredients);
            return AddableEntities(Manager.Ingredients, new HashSet<string>(currIngredients));
        }

        [Pure]
        public static IEnumerable<IShop> GetAddableShopsForIngredient(string ingredientName,
                                                                      IEnumerable<string> currShops)
        {
            AddableEntitiesContract<IIngredient, IShop>(Manager.Ingredients, ingredientName, currShops);
            return AddableEntities(Manager.Shops, new HashSet<string>(currShops));
        }

// ReSharper restore PossibleMultipleEnumeration

        [Pure]
        public static IEnumerable<IRecipe> GetAddableSubRecipes(string recipeName)
        {
            Contract.Requires(recipeName == "" || Manager.Recipes.Contains(recipeName));
            Contract.Ensures(Contract.Result<IEnumerable<IRecipe>>() != null);

            if (recipeName == "") return Manager.Recipes;
            var recipe = Manager.Recipes[recipeName];
            var unaddableRecipes = new HashSet<string> {recipeName};

            var visited = new HashSet<IRecipe>();
            Func<IRecipeStep, bool> filter = s => s.Type == StepType.SubRecipe && !visited.Contains(s.SubRecipe);
            Func<IRecipe, IEnumerable<IRecipe>> steps = r => r.Steps.Where(filter).Select(s => s.SubRecipe);

            foreach (var otherRecipe in Manager.Recipes.Where(r => r.Id != recipe.Id))
            {
                if (!visited.Add(otherRecipe)) continue;
                var queue = new Stack<IRecipe>(steps(otherRecipe));
                while (queue.Count != 0)
                {
                    var subRecipe = queue.Pop();
                    if (subRecipe.Id == recipe.Id)
                    {
                        unaddableRecipes.Add(otherRecipe.Name);
                        break;
                    }
                    foreach (var s in steps(subRecipe))
                        queue.Push(s);
                }
            }

            return AddableEntities(Manager.Recipes, unaddableRecipes);
        }

        #endregion

        [Pure]
        public static IEnumerable<IIngredient> GetAllIngredientsForCategory(string categoryName,
                                                                            IEnumerable<string> currChildren)
        {
            Contract.Requires<ArgumentNullException>(currChildren != null);
            Contract.Requires<ArgumentException>(categoryName == "" || Manager.Categories.Contains(categoryName));
            Contract.Ensures(Contract.Result<IEnumerable<ICategory>>() != null);

            if (categoryName == "") return new List<IIngredient>();
            var category = Manager.Categories[categoryName];
            var allIngredients = new SortedSet<IIngredient>(category.Ingredients, new NamedEntityComparer());
            var stack = new Stack<ICategory>(currChildren.Select(c => Manager.Categories[c]));
            while (stack.Count > 0)
            {
                category = stack.Pop();
                foreach (var ingredient in category.Ingredients)
                    allIngredients.Add(ingredient);
                foreach (var child in category.Children)
                    stack.Push(child);
            }
            return allIngredients;
        }

        [Pure]
        public static IEnumerable<IMeasure> GetOrderedMeasures(MeasureFamily family, MeasureType type)
        {
            Contract.Ensures(Contract.Result<IEnumerable<IMeasure>>() != null);

            var second = new List<IMeasure>();
            var third = new List<IMeasure>();
            foreach (var m in Manager.Measures)
            {
                if (m.Family == family)
                {
                    if (m.Type == type) yield return m;
                    else second.Add(m);
                }
                else third.Add(m);
            }
            foreach (var m in second) yield return m;
            foreach (var m in third) yield return m;
        }

        public static void RegisterStats(IEvent ev)
        {
            Contract.Requires(!ev.HasEnded);
            Contract.Ensures(ev.HasEnded);

            ev.Close();
            var date = ev.Date.GetValueOrDefault();
            foreach (var g in ev.Guests.Select(eg => eg.Guest))
            {
                foreach (var r in ev.Recipes.Select(er => er.Recipe))
                {
                    if (g.RecipeStats.Contains(r.Id)) g.RecipeStats[r.Id].Update(date);
                    else g.RecipeStats.Add(r.Name, date);
                }
            }
        }

        #region Conversion Methods

        static string AddToIngredientListQuantity(IIngredientQuantity iq, double ratio)
        {
            var ingredientName = iq.Ingredient.Name;
            var quantity = iq.Quantity*iq.Measure.ConversionFactor*ratio;
            switch (iq.Measure.Type)
            {
                case MeasureType.Weight:
                    IncreaseIngredientListQuantity(ingredientName, Grams, quantity);
                    return null;
                case MeasureType.Volume:
                    IncreaseIngredientListQuantity(ingredientName, Liters, quantity);
                    return null;
                case MeasureType.Unit:
                    IncreaseIngredientListQuantity(ingredientName, Units, quantity);
                    return null;
                default:
                    return string.Format("L'ingrediente {0} è espresso come q.b., non è possibile aggiungerlo.",
                                         ingredientName);
            }
        }

        static void IncreaseIngredientListQuantity(string ingredientName, string measureName, double quantity)
        {
            var shoppingList = Manager.ShoppingList;
            if (shoppingList.Contains(ingredientName, measureName))
            {
                var iq = shoppingList[ingredientName, measureName];
                iq.Update(measureName, iq.Quantity + quantity, iq.Notes);
                return;
            }
            shoppingList.Add(ingredientName, measureName, quantity);
        }

        static string RemoveFromIngredientListQuantity(IIngredientQuantity iq, double ratio)
        {
            var ingredientName = iq.Ingredient.Name;
            var quantity = iq.Quantity*iq.Measure.ConversionFactor*ratio;
            switch (iq.Measure.Type)
            {
                case MeasureType.Weight:
                    return DecreaseIngredientListQuantity(ingredientName, Grams, quantity);
                case MeasureType.Volume:
                    return DecreaseIngredientListQuantity(ingredientName, Liters, quantity);
                case MeasureType.Unit:
                    return DecreaseIngredientListQuantity(ingredientName, Units, quantity);
                default:
                    return string.Format("L'ingrediente {0} è espresso come q.b., non è possibile rimuoverlo.",
                                         ingredientName);
            }
        }

        static string DecreaseIngredientListQuantity(string ingredientName, string measureName, double quantity)
        {
            var shoppingList = Manager.ShoppingList;
            if (shoppingList.Contains(ingredientName, measureName))
            {
                var iq = shoppingList[ingredientName, measureName];

                var diff = iq.Quantity - quantity;
                if (diff < 0)
                    return string.Format("La lista della spesa non contiene abbastanza {0}", ingredientName);

                if (diff.Equals(0))
                    shoppingList.Remove(ingredientName, measureName);
                else
                    iq.Update(measureName, diff, iq.Notes);

                return null;
            }
            return string.Format("La lista della spesa non contiene {0}", ingredientName);
        }

        #endregion

        #region AddableEntities Helpers

        [Pure]
        static IEnumerable<TEntity> AddableEntities<TEntity>(IEnumerable<TEntity> entitySet,
                                                             ICollection<string> addedEntities)
            where TEntity : class, INamedEntity
        {
            var en = entitySet.GetEnumerator();
            var hasNext = en.MoveNext();
            while (addedEntities.Count != 0 && hasNext)
            {
                var entity = en.Current;
                if (!addedEntities.Remove(entity.Name))
                    yield return entity;
                hasNext = en.MoveNext();
            }
            while (hasNext)
            {
                yield return en.Current;
                hasNext = en.MoveNext();
            }
        }

// ReSharper disable UnusedParameter.Local

        [ContractAbbreviator]
        static void AddableEntitiesContract<TSrc, TEntity>(INamedEntities<TSrc> src, string srcName,
                                                           IEnumerable<string> addedEntities)
            where TSrc : class, INamedEntity
            where TEntity : class, INamedEntity
        {
            Contract.Requires(addedEntities != null);
            Contract.Requires(srcName == "" || src.Contains(srcName));
            Contract.Ensures(Contract.Result<IEnumerable<TEntity>>() != null);
        }

// ReSharper restore UnusedParameter.Local

        #endregion

        #region Nested type: NamedEntityComparer

        sealed class NamedEntityComparer : IComparer<INamedEntity>
        {
            #region IComparer<INamedEntity> Members

            public int Compare(INamedEntity x, INamedEntity y)
            {
                return String.Compare(x.Name, y.Name, StringComparison.Ordinal);
            }

            #endregion
        }

        #endregion
    }
}